Network visualisation with igraph, ggraph and networkD3
Welcome to this session of the seminar! In the last weeks you might have seen, that the visualisation of networks is not as easy as it may seem. This is due to the fact, that it is quite difficult to sensefully map 3D Information in a two dimensional space. The content of this chapter is deeply inspired by the materials of Katherine Ognyanova (2024) and (Rawlings et al. 2023).
When we design a network visualization, like almost always we first need to think about the purpose of the visualization. What are the structural features we want to show? What do we want to communicate?
Do we want to show the network as a whole?
Do we want to show the network structure?
Do we want to show the actors and their attributes?
Do we want to show the edges and their attributes?
Do we want to show the network dynamics?
Do we want to show communities or clusters? (next week)
Do we want to show the network in a geographical context? (We will cover that in geo visualisation)
Most of the time, we will want to show a combination of these features.
When we talk about network visualisation, there are different forms of representation, that we can use. The most common we call network maps, a visualitsation of the nodes and edges in a two-dimensional space. But we can use other forms of representation as well, like
Good old statistical charts (e.g. bar charts, histograms, boxplots, tables, etc.)
Arc diagrams
Heat maps
etc.
Today we will focus mostly on network maps.
When mapping networks we have different design-elements, that we can use to represent the nodes and edges. The most common are: Color, Position, Size, Shape and Position as well as labels.
Colors in R
Colors are extremely useful in plotting to differentiate between types of objects or different values of variabes.
Colors can be called by using either named colors, hex or RGB values.
In base R, we can plot by defining point coordinates, symbol shapes, point size and color:
You can use built-in R colors by using the command colors().
Sometimes we need a number of contrastiing colors. We can use predefined color palettes or the RColorBrewer package.
Code
par(bg="white")pal1 <-heat.colors(5,alpha=1 ) # 5 colors from the heat palette, opaquepal2 <-rainbow(5,alpha=.5 ) # 5 colors from the heat palette, transparentplot(x=1:10,y=1:10,pch=19,cex=5,col=pal1 )par(mfrow =c(4, 1) ) # Set up the plotting area to have 3 rows and 1 columnplot(x=1:10,y=1:10,pch=19,cex=5,col=pal2 )# Generate our own palette and plotpalf <-colorRampPalette(c("lavender","lightblue") ) plot(x=1:10,y=1:10,pch=19,cex=5,col=palf(10) ) palf <-colorRampPalette(c(rgb(1,1,1, .2),rgb(.8,0,0, .7)),alpha=TRUE)plot(x=10:1,y=1:10,pch=19,cex=5,col=palf(10) ) library(RColorBrewer)display.brewer.all() # shows all palettespar(mfrow =c(1, 1)) # Reset the plotting area to default
Heat-palette
Rainbow-palette
Self-defined palette
Basic visualisation in ‘igraph’
Remember the twin-city network, you created in the last Übungsblatt? We will use it again to day in order to show the different options we have in plotting.
As we can clearly see, it is really difficult to identify any nodes or edges, and especially any structure within the network. But already with a few lines of code, we can improve the visualization a lot and make structural metrics more obvious.
Plotting parameters
Lets have a look at the (most important) different options:
Parameter
Description
NODES
vertex.color
Node color
vertex.frame.color
Node border color
vertex.shape
Node shape options include “none”, “circle”, “square”, “csquare”, “rectangle”, “crectangle”, “vrectangle”, “pie”, “raster”, or “sphere”
vertex.size
Size of the node (default is 15)
vertex.size2
Second size of the node (e.g., for a rectangle)
vertex.label
Character vector used to label the nodes
vertex.label.family
Font family of the label (e.g., “Times”, “Helvetica”)
Edge curvature, range 0-1 (FALSE sets it to 0, TRUE to 0.5)
arrow.mode
Vector specifying arrow presence: 0 no arrow, 1 back, 2 forward, 3 both
OTHER
margin
Empty space margins around the plot, vector with length 4
frame
If TRUE, the plot will be framed
main
Adds a title to the plot if set
sub
Adds a subtitle to the plot if set
asp
Aspect ratio of the plot (y/x), numeric
palette
Color palette to use for vertex color
rescale
Whether to rescale coordinates to [-1,1], default is TRUE
There are two ways to define the attributes for a plot. We can either define them directly in the plot prompt or add the information to the igraph-object directly.
tc <- twin_citiesleipzig_vertex <-which(V(tc)$name =="Leipzig") # Find the index of Leipzig# Calculate shortest path distances from Leipzig to all other nodesdistances <-distances(tc, v = leipzig_vertex)# Färbe die Knoten basierend auf der Entfernung von LeipzigV(tc)$color[distances ==1] <-"lightblue"# Wenn Abstand 1, Farbe "lightblue"V(tc)$color[distances ==2] <-"lightcyan"# Wenn Abstand 2, Farbe "lightcyan"V(tc)$size <-4V(tc)$label <-NAE(tc)$edge.color <-"gray80"plot(tc)legend(x="bottomleft",c("dist = 1","dist = 2"),pch=21,col="#777777",pt.bg=c("lightblue", "lightcyan"),pt.cex=2,cex=.8,bty="n",ncol=1)
Saving parameters in igraph-objects directly
or we can only plot names (this time for a subset of nodes for visibility)
Code
summary(V(tc)$color)V(tc)$color[V(tc)$name =="Leipzig"] <-"lightblue"lightblue_nodes <-V(tc)[which(V(tc)$color =="lightblue")]V(tc)$label <-V(tc)$name# Create the subgraph with those nodeslightblue_subgraph <-induced_subgraph(tc, lightblue_nodes)# called aus irgendeinem grund das falsche igraph - so gehtslightblue_subgraph <- igraph::simplify(lightblue_subgraph,remove.multiple =TRUE,remove.loops =TRUE)plot(lightblue_subgraph,vertex.shape="none",vertex.label=V(lightblue_subgraph)$name, vertex.label.font=2,vertex.label.color="gray40",vertex.label.cex=.7,edge.color="gray85" )
Plot only labels
and then again we can overwrite attributes in the plot directly.
Plotting layouts
The package igraph offers a variety of layouts. Depending on the size of the network, we can already see a lot of differences in the plot by changing the layout. The default value is layout_nicely, a smart function that chooses a layouter based on the graph.
Igraph has a lot of built in layouts that are either a function or a numeric matrix, that specify how the vertices will be placed in a plot.
If it is a numeric matrix, then the matrix has to have one line for each vertex, specifying its coordinates. The matrix should have at least two columns, for the x and y coordinates, and it can also have third column, this will be the z coordinate for 3D plots and it is ignored for 2D plots.
If a two column matrix is given for the 3D plotting function rglplot then the third column is assumed to be 1 for each vertex.
If layout is a function, this function will be called with the graph as the single parameter to determine the actual coordinates. The function should return a matrix with two or three columns. For the 2D plots the third column is ignored.
The Fruchterman-Reingold algorithm is a popular force-directed layout method. It simulates a physical system where nodes act as repelling particles and edges as attracting springs. This results in a graph where nodes are evenly distributed, with more connected nodes closer together. However, it can be slow and is typically not used for graphs larger than ~1000 vertices.
Code
plot(twin_cities,layout=layout_with_fr)
With force-directed layouts we can use the niter parameter to control the number of iterations to perdorm. The default is set at 500.
FR-Layout with higher number of iterations (niter)
By default, plot coordinates are rescaled to the [-1, 1] interval. To adjust this, set rescale=FALSE and manually rescale the plot using a scalar. You can also use norm_coords to normalize the plot within custom boundaries for a more compact or spread-out layout.
Code
l <-layout_with_fr(twin_cities) # save coordinates so layout is not recalculated# Normalize coordinates to custom boundariesl <-norm_coords(l, ymin=-1, ymax=1, xmin=-1, xmax=1)# Set up plot gridpar(mfrow=c(2,2), mar=c(0,0,0,0))# Plot with varying layoutsplot(twin_cities, rescale=FALSE, layout=l*0.4)plot(twin_cities, rescale=FALSE, layout=l*0.6)plot(twin_cities, rescale=FALSE, layout=l*0.8)plot(twin_cities, rescale=FALSE, layout=l*1.0)
FR with Norm-Coords
The Kamada-Kawai algorithm is another force-directed algorithm that minimizes the energy in a spring system.
Code
plot(twin_cities,layout=layout_with_kk)
Kamada-Kawaii
The graphopt layout algorithm allows customization of physical simulation parameters that influence the resulting graph layout. You can adjust:
charge — the electric repulsion between nodes (default: 0.001)
mass — the mass of each node, affecting movement (default: 30)
spring.length — the ideal edge length (default: 0)
spring.constant — the stiffness of the springs (default: 1)
Tweaking these parameters can produce significantly different layouts by changing how nodes repel or attract each other.
The MDS (Multidimensional Scaling) layout positions nodes based on a distance or similarity measure. By default, it uses shortest path distances, placing more similar nodes closer together. You can also supply a custom distance matrix using the dist parameter. MDS layouts are useful because node positions reflect actual distances, giving the layout a clear geometric interpretation. However, visual clarity can suffer — nodes may overlap or cluster too tightly.
Code
plot(twin_cities,layout = layout_with_mds)
So there are a lot of different ways to plot networks - highlighting different structural properties to the visual analysis (or description).
Code
# Get all layout functions, except the first (which is usually NULL) and bipartite layoutslayouts <-grep("^layout_", ls("package:igraph"), value =TRUE)[-1]layouts <- layouts[!grepl("bipartite", layouts)]# Folder to save plotsdir.create("Graphics/layouts", showWarnings =FALSE)# Plot and save each layoutfor (layout in layouts) { layout_fun <-get(layout, asNamespace("igraph")) l <-layout_fun(twin_cities)png(filename =paste0("Graphics/layouts/", layout, ".png"), width =800, height =800)plot(twin_cities,edge.arrow.mode =0,layout = l,main = layout)dev.off()}# Start Quarto blockcat("::: {layout-ncol=4}\n\n")# Print each image markdownfor (layout in layouts) {cat(sprintf('{group="distribution"}\n\n', layout))}# End Quarto blockcat(":::\n")
Higlighting specific nodes or links
This plot visualizes how far each city is from Leipzig in terms of network distance. A lavender-to-blue gradient is used, where cities closer to Leipzig appear darker and those further away appear lighter. The path distance to Leipzig is added as a vertex.label.
This visualization highlights the shortest path between Leipzig and Kyoto to emphasize both the nodes and the edges involved in the path.
Code
city_path <-shortest_paths(twin_cities, from =V(twin_cities)[name =="Leipzig"], to =V(twin_cities)[name =="Kyoto"],output ="both")ecol <-rep("gray80", ecount(twin_cities))ecol[unlist(city_path$epath)] <-"#9370DB"# medium purplevcol <-rep("gray40", vcount(twin_cities))vcol[unlist(city_path$vpath)] <-"#D8BFD8"# thistle (light purple)plot(twin_cities, vertex.color = vcol, edge.color = ecol, edge.width =0.1 )
Path between Leipzig and Kyoto
This figure highlights all edges that are directly connected to Leipzig. The city itself is shown in purple, and its incident connections are marked in blue.
These two side-by-side plots mark groups of cities based on their country attribute (France or Japan) - this will make more sense once we dive into the community detection algorithms next week :)
Code
# **Mark country groups (France and Japan) with lavender-blue tones**france_nodes <-which(V(twin_cities)$countries =="France")japan_nodes <-which(V(twin_cities)$countries =="Japan")par(mfrow =c(1, 2))plot(twin_cities, mark.groups = france_nodes, mark.col ="#E6E6FA", # lavendermark.border =NA)plot(twin_cities, mark.groups =list(france_nodes, japan_nodes), mark.col =c("#E6E6FA", "#B0C4DE"), # lavender and light steel bluemark.border =NA)dev.off()
You can customize the layout. Here we use "gem", a force-directed layout similar to Fruchterman-Reingold, which gives a more organic structure to the network. You can use different edge and node geometries for various effects. geom_edge_fan() helps visualize overlapping edges, and you can control their appearance with color, width, and transparency.
Code
ggraph(twin_cities, layout ="gem") +geom_edge_fan(size =1, color ="lightgray") +geom_node_point(size =4, color ="lavender") +ggtitle("Twin Cities Network (GEM Layout)") +theme_void()
GGraph-Plot
You can also use layouts like 'linear' to arrange nodes along a line, which is useful for timelines or flows. Here we use geom_edge_arc() to show curved connections.
Just like in ggplot2, you can map node or edge attributes to aesthetics using aes(). Below we color edges by a type attribute (if available) and scale node size
Code
# Save high-resolution ggraph plotp <-ggraph(twin_cities, layout ="linear") +geom_edge_arc(color ="#6495ED", width =0.6) +geom_node_point(size =1, color ="navy") +geom_node_text(aes(label = name), nudge_y =-0.6,vjust =1.5,angle =45,size =1,color ="navy") +theme_void()# Save the plotggsave("Graphics/twin_cities_linear.png", plot = p, width =10, height =6, dpi =300, units ="in")
Linear Plot
You can also label nodes using their names. Here we use geom_node_text() with repelling to avoid overlapping labels.
Code
ggraph(twin_cities, layout ="fr") +geom_edge_link(color ="gray80") +geom_node_point(size =5, color ="lavender") +# medium orchidgeom_node_text(aes(label = name), size =3, color ="navy", repel =TRUE) +theme_void()
Repel labels
The ggraph package automatically generates legends when aesthetics like color or size are mapped to attributes. This makes interpreting your network plots easier without manual legend handling.
Code
# Create the network plot with ggraphnetwork_plot <-ggraph(twin_cities, layout ="fr") +geom_edge_link(alpha =0.4) +geom_node_point(aes(color = countries), size =2) +theme_minimal() +labs(title ="Twin Cities Network by Country") +theme(legend.position ="right", # Keeps the legend on the rightplot.margin =margin(r =150), # Adds margin space for legendlegend.text =element_text(size =3), # Smaller text for legendlegend.title =element_text(size =2), # Smaller legend title textlegend.key.size =unit(0.5, "cm") # Smaller legend key size (color swatches) )# Arrange the plot with the adjusted legend sizegrid.arrange(network_plot, ncol =1)
Color by country
Other representatives
Code
# Convert the twin_cities network to an adjacency matrix (without edge weights, if not available)netm <-as_adjacency_matrix(twin_cities, sparse =FALSE)# Set the row and column names as the city names (or node names)colnames(netm) <-V(twin_cities)$namerownames(netm) <-V(twin_cities)$name# Create a color palette for the heatmappalf <-colorRampPalette(c("lavender", "lightblue"))# Plot the heatmapheatmap(netm, Rowv =NA, Colv =NA, col =palf(100), scale ="none", margins =c(10, 10),main ="Adjacency Matrix Heatmap of Twin Cities Network")
Code
# Calculate the degree distribution of the networkdeg.dist <-degree_distribution(twin_cities, cumulative =TRUE, mode ="all")# Plot the degree distributionplot(x =0:max(degree(twin_cities)), y =1- deg.dist, pch =19, cex =1.2, col ="lavender", xlab ="Degree", ylab ="Cumulative Frequency",main ="Degree Distribution of Twin Cities Network")
Code
# Create a random network (Erdős–Rényi model)set.seed(123) # For reproducibilityrandom_net <-erdos.renyi.game(20, p =0.2, directed =FALSE) # 20 nodes, edge probability = 0.2# Convert the random network to an adjacency matrixnetm <-as_adjacency_matrix(random_net, sparse =FALSE)# Set row and column names as the node names (1, 2, ..., 20)colnames(netm) <-V(random_net)$namerownames(netm) <-V(random_net)$name# Create a color palette for the heatmappalf <-colorRampPalette(c("lavender", "lightblue"))# Plot the heatmap of the adjacency matrixheatmap(netm, Rowv =NA, Colv =NA, col =palf(100), scale ="none", margins =c(10, 10),main ="Adjacency Matrix Heatmap of Random Network")# Calculate the degree distribution of the random networkdeg.dist <-degree_distribution(random_net, cumulative =TRUE, mode ="all")# Plot the degree distributionplot(x =0:max(degree(random_net)), y =1- deg.dist, pch =19, cex =1.2, col ="lavender", xlab ="Degree", ylab ="Cumulative Frequency",main ="Degree Distribution of Random Network")
Degree distribution
Heatmap
Network evolution visualisation
A lot of the times, networks are plottes as static networks at different timeframes
# Set the starting node (Leipzig)start_node <-"Leipzig"# Calculate distance from Leipzig for all nodesdistances_from_leipzig <-distances(g, v = start_node)[1,]V(g)$distance <- distances_from_leipzig # Store the distances as a vertex attribute# Create a list of graphs for each distance level (time)max_dist <-max(V(g)$distance, na.rm =TRUE) # Maximum distance# Create a list to store subgraphs at each time stepsubgraphs <-lapply(0:max_dist, function(d) {# Create a subgraph that includes nodes with distance <= d nodes_in_time <-V(g)[V(g)$distance <= d]induced_subgraph(g, vids = nodes_in_time)})# Create an animated plot using ggraphp <-ggraph(g, layout ="fr") +geom_edge_link(aes(alpha =0.3), show.legend =FALSE) +geom_node_point(aes(color = distance), size =5) +geom_node_text(aes(label = name), vjust =-1) +transition_states(V(g)$distance, transition_length =1, state_length =100) +labs(title ="Twin City Growth from Leipzig: t = {closest_state}") +ease_aes('cubic-in-out')# Animate the graph over timeanimation <-animate(p, nframes = (max_dist +1) *10, width =800, height =600)anim_save("Graphics/twin_city_growth.gif", animation = animation)
Growth Gif
or we can just create a shiny-App with a slider
Code
library(shiny)ui <-fluidPage(sliderInput("t", "Time Step", min =0, max =2, value =0, step =1),plotOutput("networkPlot"))server <-function(input, output, session) { output$networkPlot <-renderPlot({ vids <-V(g)[V(g)$dist <= input$t] g_sub <-induced_subgraph(g, vids = vids)ggraph(g_sub, layout ="fr") +geom_edge_link(aes(alpha =0.3), show.legend =FALSE) +geom_node_point(aes(color = distance), size =5) +geom_node_text(aes(label = name), vjust =-1) +ggtitle(paste("Network at time =", input$t)) })}shinyApp(ui, server)
Interactive visualisation with networkD3
To create an interactive network visualization with networkD3, you first need to convert your igraph network into two data frames: one for nodes and one for edges (links). The nodes should include a unique id and optionally attributes like group or size, while the links should include source and target indices starting from 0 (not 1).
Once the data is prepared, use forceNetwork() to build the visualization, specifying node and edge attributes, and finally save it as an HTML file using htmlwidgets::saveWidget(). This allows zooming, dragging, and exploring the network interactively in a browser.
Code
# Extract links and nodes for visualizationlinks <-as.data.frame(get.edgelist(twin_cities))nodes <-data.frame(id =1:vcount(twin_cities),name =V(twin_cities)$name,countries =V(twin_cities)$countries )# Ensure node IDs are numeric and start from 0links.d3 <-data.frame(from =as.numeric(factor(links$V1)) -1,to =as.numeric(factor(links$V2)) -1)# Add a Nodesize column (you can use an actual attribute here, like degree, if needed)nodes.d3 <-data.frame(idn =factor(nodes$name, levels = nodes$name), nodes)nodes.d3$Nodesize <-rep(6, nrow(nodes.d3)) # Add a constant size for each node# Save as a standalone HTML filehtmlwidgets::saveWidget(forceNetwork(Links = links.d3, Nodes = nodes.d3, Source ="from", Target ="to",NodeID ="name", Group ="countries", linkWidth =1,linkColour ="#afafaf", fontSize =12, zoom =TRUE, legend =FALSE,Nodesize ="Nodesize", opacity =0.8, charge =-300, width =800, height =600),"Graphics/network_plot_interactive.html", selfcontained =TRUE)
Exercise
The Bali-network shows the interactions inside the Jemaah Ismlamiyah terrorist group, which conducted an attack in Bali in the year of 2002 (Koschade 2007).
The network consists of 17 nodes (the terrorists) and 63 edges (their interactions). Next to the names, their roles in the group are saved as node attributes. The intensity of the interaction is saved as an edge attribute (See help(Bali)).
Plot the network so that the role is displayed as the node label. You can also change the size of the node.
Create an interactive plot, where, when you hover over the node, the role is shown.
Code
# Extract links and nodes for visualization# Extract edge list and include weightsedges <-get.edgelist(Bali)weights <-E(Bali)$IC # assuming edge weights are stored as 'weight'links <-data.frame(from = edges[, 1],to = edges[, 2],value = weights # 'value' is the standard name used by networkD3 for link width)# Convert node names to zero-based indices for networkD3links.d3 <-data.frame(from =as.numeric(factor(links$from)) -1,to =as.numeric(factor(links$to)) -1,value = links$value)nodes <-data.frame(id =1:vcount(Bali),name =V(Bali)$vertex.names,role =V(Bali)$role )# Full role name in data.framenodes <- nodes %>%mutate(role =recode(role,"CT"="Command Team","OA"="Operational Assistant","BM"="Bomb Maker","SB"="Suicide Bomber","TL"="Team Lima" ) )# Add a Nodesize column (you can use an actual attribute here, like degree, if needed)nodes.d3 <-data.frame(idn =factor(nodes$name, levels = nodes$name), nodes)nodes.d3$Nodesize <-rep(6, nrow(nodes.d3)) # Add a constant size for each nodeforceNetwork(Links = links.d3, Nodes = nodes.d3, Source ="from", Target ="to",NodeID ="role", Group ="role", linkWidth = (links.d3$value*3),linkColour ="#afafaf", fontSize =12, zoom =TRUE, legend =TRUE,Nodesize ="Nodesize", opacity =0.8, charge =-300, width =800, height =600)
References
Koschade, Stuart Andrew. 2007. “The Internal Dynamics of Terrorist Cells: A Social Network Analysis of Terrorist Cells in an Australian Context.” PhD thesis, Queensland University of Technology.
Ognyanova, Katya. 2024. “Static and Dynamic Network Visualization with R.”Katya Ognyanova.
Rawlings, Craig M., Daniel A. McFarland, James Moody, and Jeffrey A. Smith, eds. 2023. “How Are Social Network Data Visualized?” In Network Analysis: Integrating Social Network Theory, Method, and Application with R, 88–114. Structural Analysis in the Social Sciences. Cambridge: Cambridge University Press. https://doi.org/10.1017/9781139794985.006.
Copyright
Copyright Leonie Steinbrinker, 2025. All Rights Reserved